oasis
2022-04-14 ยท 13 min read
Site: https://oasisprotocol.org/
Github: https://github.com/oasisprotocol/, oasis-sdk, oasis-core
Lots of interesting/useful SGX-related stuff
Oasis Run a Node Process (Setting up SGX)
Run an Intel Attestation Service (IAS) proxy
TODO: enclave rpc
Core Oasis Runtime Model #
Oasis Core Compute Nodes run user executables inside SGX enclaves.
____________________
| ________________ |
| | Runtime Loader | |
| '----------------' |
| Enclave |
______ | ________________ |
| Node |<------>| Runtime Bin | |
'______' | '----------------' |
'--------------------'
- The Rust
oasis-core/runtime
is the in-enclave runtime. This is "Runtime Loader" and "Runtime Bin" in the above diagram. - The Go
oasis-core/go/runtime/host
is responsible for provisioning enclaves and then communicating with them. This is part of "Node" in the above diagram. - The Node and Runtime communicate via
- Node <-> Unix Socket <-> Enclave Runner <-> Fortanix shared-memory FIFO queue <-> Enclave Runtime Binary
oasis-core Runtime Loader #
https://github.com/oasisprotocol/oasis-core/tree/master/runtime-loader
Inputs:
- Runtime Filename
- Signature
- host-socket
SgxsLoader
- Opens the SGX kernel devices for loading enclaves.
- Opens client connection to AESM service for the EINITTOKEN provider.
- Build enclave from file, SIGSTRUCT signature, and their custom adapter, which just routes
connect
syscalls from the enclave to a local Unix Domain Socket (UDS). This connection is the bidirectional arrow betweenNode
andRuntime Bin
in the above diagram.
/// SGX runtime loader.
pub struct SgxsLoader;
impl Loader for SgxsLoader {
fn run(
&self,
filename: String,
signature_filename: Option<&str>,
host_socket: String,
) -> Fallible<()> {
let sig = match signature_filename {
Some(f) => f,
None => {
return Err(format_err!("signature file is required"));
}
};
// Spawn the SGX enclave.
let mut device = IsgxDevice::new()?
.einittoken_provider(AesmClient::new())
.build();
let mut enclave_builder = EnclaveBuilder::new(filename.as_ref());
enclave_builder.signature(sig)?;
enclave_builder.usercall_extension(HostService::new(host_socket));
let enclave = enclave_builder.build(&mut device)?;
enclave.run()
}
}
oasis-core Host #
// Manifest is a deserialized runtime bundle manifest.
type Manifest struct {
// Name is the optional human readable runtime name.
Name string `json:"name,omitempty"`
// ID is the runtime ID.
ID common.Namespace `json:"id"`
// Version is the runtime version.
Version version.Version `json:"version,omitempty"`
// Executable is the name of the runtime ELF executable file.
Executable string `json:"executable"`
// SGX is the SGX specific manifest metadata if any.
SGX *SGXMetadata `json:"sgx,omitempty"`
// Digests is the cryptographic digests of the bundle contents,
// excluding the manifest.
Digests map[string]hash.Hash `json:"digests"`
}
// SGXMetadata is the SGX specific manifest metadata.
type SGXMetadata struct {
// Executable is the name of the SGX enclave executable file.
Executable string `json:"executable"`
// Signature is the name of the SGX enclave signature file.
Signature string `json:"signature"`
}
// Bundle is a runtime bundle instance.
type Bundle struct {
Manifest *Manifest
Data map[string][]byte
}
// RuntimeBundle is a exploded runtime bundle ready for execution.
type RuntimeBundle struct {
*bundle.Bundle
// Exeuctable is the path to the extracted ELF or TEE executable.
Path string
}
// Config contains common configuration for the provisioned runtime.
type Config struct {
// Bundle is the runtime bundle.
Bundle *RuntimeBundle
// Extra is an optional provisioner-specific configuration.
Extra interface{}
// MessageHandler is the message handler for the Runtime Host Protocol messages.
MessageHandler protocol.Handler
// LocalConfig is the node-local runtime configuration.
LocalConfig map[string]interface{}
}
Runtime Host Protocol #
This is the core API layer available for executables running inside an SGX enclave. Nodes communicate bidirectionalyl with the running user binary via a bespoke length-prefixed CBOR message stream protocol.
Connection Lifecycle #
Uninitialized
- Newly created connection. Must be initialized before use.Inititalizing
- Parties exchange version and feature info.Ready
- The versions and features are negotiated; however, we first need to perform remote attestation before running stuff in the enclave.Closed
- Connection is closed.
Remote Attestation #
Host attestation flow - oasis-core/go/runtime/host/sgx/sgx.go
- (Host) get the Quoting Enclave (QE)'s QuoteInfo from the AESM.
- (Host) get the Service Provider ID (SPID) from the Intel Attestation Services (IAS) cache.
- (Host) send a
RuntimeCapabilityTEERakInitRequest { QuoteInfo.TargetInfo }
request to the enclave. - (Enclave) receive the request. initialize the Runtime Attestation Key (RAK) with the
TargetInfo
and then generate a new ephemeral RAK keypair (if one doesn't exist already). - (Host) ask the IAS cache for the latest EPID revocation list (since Oasis uses EPID).
- (Host) send a
RuntimeCapabilityTEERakReportRequest {}
to the enclave. - (Enclave) receive the request.
- Sample a random
nonce
. - Generate
report_body = H(RAK_pub)
. This binds the RAK pubkey to the report. - Set
report_data = report_body || nonce
. - Generate
report = Report::for_target(target_info, report_data)
from the enclaveEREPORT
instruction. See: Report (EREPORT). - Return the RAK public key,
report
, andnonce
to the host.
- Sample a random
- (Host) Request a Quote from the AESM using the
report
, SPID, andsigRL
revocation list. For some reason thenonce
is empty here? Not sure what the nonce here is for... - (Host) Verify that the
quote.Report
contains a MRENCLAVE and MRSIGNER that we expect. This ensures we're talking to the right application enclave. - (Host) Verify that the
Quote.Report.Attributes.is_debug == config.is_debug
. That way we don't trust debug enclaves in production and don't use production enclaves in testing. - (Host) Ask the IAS to Verify
Evidence { quote, nonce }
. IAS will return an Attestation Verification Report (AVR) to us. - (Host) send a
RuntimeCapabilityTEERakAvrRequest { avr }
request to the enclave, to pass it the AVR.- (Q: why does the enclave need to verify the AVR? shouldn't the client do this? I suppose we already checked that the enclave is the one we expect; in which case, we trust it to verify the AVR correctly.)
- (Enclave) receive the
AVR
in the request.- Expect the
nonce
in the AVR to match the one we generated above in step (7.1). - See Enclave Attestation Verification Report AVR handling.
- Verify the current measured Report's enclave identity matches the enclave identity in the
AVR
. - Verify that the RAK pubkey matches the one in the
AVR.report_data
. - Verify that the
nonce
also matches the one in theAVR.report_data
. - Ratchet our internal timestamp from the one we've observed.
- Expect the
- (Host) we've now fully verified the enclave. package up the RAK pubkey and AVR into a
capabilityTEE
.
Other nodes can also verify the capabilityTEE
by
- checking the
AVR
against the Intel Trust Root CA - checking the
MRENCLAVE
/MRSIGNER
are what we expect - checking that the
AVR.Quote.Report.ReportData
includes the expected RAK pubkey
type SPIDInfo struct {
SPID ias.SPID
QuoteSignatureType ias.SignatureType
}
// A deserialized AVRBundle from IAS.
type AttestationVerificationReport struct {
ID string `json:"id"`
Timestamp string `json:"timestamp"`
Version int `json:"version"`
ISVEnclaveQuoteStatus ISVEnclaveQuoteStatus `json:"isvEnclaveQuoteStatus"`
ISVEnclaveQuoteBody []byte `json:"isvEnclaveQuoteBody"`
RevocationReason *CRLReason `json:"revocationReason"`
PSEManifestStatus *PSEManifestStatus `json:"pseManifestStatus"`
PSEManifestHash string `json:"pseManifestHash"`
PlatformInfoBlob string `json:"platformInfoBlob"`
Nonce string `json:"nonce"`
EPIDPseudonym []byte `json:"epidPseudonym"`
AdvisoryURL string `json:"advisoryURL"`
AdvisoryIDs []string `json:"advisoryIDs"`
}
See: Quoting Enclave's QuoteInfo
Enclave Attestation Verification Report (AVR) handling #
https://github.com/oasisprotocol/oasis-core/blob/master/runtime/src/common/sgx/avr.rs
// AVR signature validation constants.
const IAS_TRUST_ANCHOR_PEM: Vec<u8> = parse_x509_pem(
r#"-----BEGIN CERTIFICATE-----
MIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV
..
DD+gT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R+mJTLwPXVMrv
DaVzWh5aiEx+idkSGMnX
-----END CERTIFICATE-----"#
).unwrap();
const PEM_CERTIFICATE_LABEL: &str = "CERTIFICATE";
const IAS_TS_FMT: &str = "%FT%T%.6f";
/// Attestation verification report.
#[derive(Debug, Clone, cbor::Encode, cbor::Decode)]
pub struct AVR {
// json blob
//
// { "isvEnclaveQuoteStatues": __
// , "isvEnclaveQuoteBody": __
// , "timestamp": __
// , "nonce": __
// }
pub body: Vec<u8>,
pub signature: Vec<u8>,
pub certificate_chain: Vec<u8>,
}
/// Enclave identity.
#[derive(Debug, Clone, Hash, Eq, PartialEq, cbor::Encode, cbor::Decode)]
pub struct EnclaveIdentity {
pub mr_enclave: MrEnclave,
pub mr_signer: MrSigner,
}
/// Authenticated information obtained from validating an AVR.
#[derive(Debug, Clone)]
pub struct AuthenticatedAVR {
pub report_data: Vec<u8>,
// TODO: add other av report/quote body/report fields we want to give
// the consumer
pub identity: EnclaveIdentity,
pub timestamp: i64,
pub nonce: String,
}
/// Decoded quote body.
#[derive(Default, Debug)]
struct QuoteBody {
version: u16,
signature_type: u16,
gid: u32,
isv_svn_qe: u16,
isv_svn_pce: u16,
basename: [u8; 32],
report_body: Report,
}
impl AVR {
pub fn verify(&self) -> Result<AuthenticatedAVR> {
// 1. read configs if we should skip verification during
// fuzzing/testing.
// 2. validate_avr_signature(self.cert_chain, self.body,
// self.sig. time::now())
// 3. check if timestamp in AVR is too old
// 4. verify ISV Enclave Quote Status == OK | SW_HARDENING_NEEDED
// if lax verification enabled, also allow some other errors
// 5. base64 decode ISV Enclave Quote Body, then decode to `QuoteBody`
// 6. Possibly allow debug enclaves if appropriate config.
// 7. They appear to use the IAS timestamp as a semi-trusted source
// and ratchet their local clock forward if the IAS timestamp is
// farther ahead.
}
}
fn validate_avr_signature(
cert_chain: &[u8],
message: &[u8],
signature: &[u8],
unix_timestamp: u64,
) -> Result<()> {
// * They note that they're doing some funky process that barely resembles
// the standard cert verification process.
// * Don't do cert revocation checks.
// * Intel Docs: The AVR is signed by the Report Signing Key using
// RSA-SHA256
// * Intel Docs: The Report Signing pubkey is distributed via an x509
// cert. This cert is a leaf cert issued by the "Attestation Report
// Signing CA".
// * A PEM-encoded cert chain is (1) the Report Signing Key Cert and (2)
// the Report Signing CA.
// 1. PEM decode the cert chain to DER. Check that each cert has the
// "CERTIFICATE" label.
// 2. Check that only two certs were returned.
// 3. Ensure the last cert is the Report Signing CA (IAS_TRUST_ANCHOR).
// 4. Ensure the CA cert is not expired, is a CA cert, is self-signed.
// 5. Parse the leaf cert, verify the usual stuff, check CA sig.
// 6. Pull out the leaf pubkey. Verify `signature` on `message`. The
// pubkey should be an RSA PKCS1 DER-encoded pubkey. The sig should be
// RSA-SHA256 PKCS1v15.
}
Runtime Attestation Key (RAK) #
RAK is the identity of the enclave, used to sign remote attestations.
/// The runtime attestation key (RAK) represents the identity of the enclave
/// and can be used to sign remote attestations. Its purpose is to avoid
/// round trips to IAS for each verification as the verifier can instead
/// verify the RAK signature and the signature on the provided AVR which
/// RAK to the enclave.
pub struct RAK {
inner: RwLock<Inner>,
}
struct Inner {
private_key: Option<PrivateKey>,
avr: Option<Arc<avr::AVR>>,
avr_timestamp: Option<i64>,
#[allow(unused)]
enclave_identity: Option<avr::EnclaveIdentity>,
#[allow(unused)]
target_info: Option<Targetinfo>,
#[allow(unused)]
nonce: Option<String>,
}
EGETKEY #
https://github.com/oasisprotocol/oasis-core/blob/master/runtime/src/common/sgx/egetkey.rs
- KMac: SHA3-256 Keccak MAC
/// egetkey returns a 256 bit key suitable for sealing secrets to the
/// enclave in cold storage, derived from the results of the `EGETKEY`
/// instruction. The `context` field is a domain separation tag.
///
/// Note: The key can also be used for other things (eg: as an X25519
/// private key).
pub fn egetkey(key_policy: Keypolicy, context: &[u8]) -> [u8; 32] {
let mut k = [0u8; 32];
// Obtain the per-CPU package SGX sealing key, with the requested
// policy.
let master_secret = egetkey_impl(key_policy, context);
// Expand the 128 bit EGETKEY result into a 256 bit key, suitable
// for use with our MRAE primitives.
let mut kdf = KMac::new_kmac256(
&master_secret,
b"Ekiden Expand SGX Seal Key",
);
kdf.update(context);
kdf.finalize(&mut k);
k
}
/// The SGX impl
fn egetkey_impl(
// MRSIGNER or MRENCLAVE
key_policy: Keypolicy,
// the domain separation value
context: &[u8],
) -> [u8; 16] {
// As we can see, using the Keyrequest::default() means oasis-core doesn't
// bind the key
let mut req = Keyrequest::default();
req.keyname = Keyname::Seal as u16;
req.keypolicy = key_policy;
let mut sha3 = Sha3::v256();
sha3.update(context);
let mut k = [0; 32];
sha3.finalize(&mut k);
req.keyid = k;
// Fucking sgx_isa::Attributes doesn't have a -> [u64;2].
// SGX_FLAGS_INITTED | SGX_FLAGS_DEBUG | SGX_FLAGS_MODE64BIT
req.attributemask[0] = 1 | 2 | 4;
// SGX_XFRM_LEGACY
req.attributemask[1] = 3;
match req.egetkey() {
Err(e) => panic!("EGETKEY failed: {:?}", e),
Ok(k) => k,
}
}
Sealing #
https://github.com/oasisprotocol/oasis-core/blob/master/runtime/src/common/sgx/seal.rs
- DeoxysII - Some new symmetric key AEAD cipher that uses AESNI. Not sure why they use this and not just like AES-GCM or AES-SIV. Maybe safer nonce reuse protection?
/// Query the enclave for sealing key material with the current context and
/// key policy, then generate a DeoxysII symmetric key using the key material.
fn new_d2(key_policy: Keypolicy, context: &[u8]) -> DeoxysII {
let mut seal_key = egetkey(key_policy, context);
let d2 = DeoxysII::new(&seal_key);
seal_key.zeroize();
d2
}
/// Seal a secret to the enclave.
pub fn seal(
/// MRSIGNER or MRENCLAVE
key_policy: Keypolicy,
/// The domain separation value
context: &[u8],
/// The plaintext data we want to seal
data: &[u8],
) -> Vec<u8> {
let mut rng = OsRng {};
let mut nonce = [0u8; NONCE_SIZE];
rng.fill(&mut nonce);
let d2 = new_d2(key_policy, context);
let mut ciphertext = d2.seal(&nonce, data.to_vec(), vec![]);
ciphertext.extend_from_slice(&nonce);
// ciphertext = [ E_k[data] || MAC tag || nonce ]
ciphertext
}
/// Unseal a previously sealed secret to the enclave.
///
/// # Panics
///
/// All parsing and authentication errors of the ciphertext are fatal and
/// will result in a panic.
pub fn unseal(
/// MRSIGNER or MRENCLAVE
key_policy: Keypolicy,
/// The domain separation value
context: &[u8],
/// Previously sealed data
ciphertext: &[u8],
) -> Option<Vec<u8>> {
let ct_len = ciphertext.len();
if ct_len == 0 {
return None;
}
assert!(
ct_len >= TAG_SIZE + NONCE_SIZE,
"ciphertext is corrupted, invalid size"
);
let ct_len = ct_len - NONCE_SIZE;
// split: ciphertext = [ E_k[data] || MAC tag || nonce ]
let mut nonce = [0u8; NONCE_SIZE];
nonce.copy_from_slice(&ciphertext[ct_len..]);
let ciphertext = &ciphertext[..ct_len];
let d2 = new_d2(key_policy, context);
let plaintext = d2
.open(&nonce, ciphertext.to_vec(), vec![])
.expect("ciphertext is corrupted");
Some(plaintext)
}
Key Manager Enclaves #
They have some keymanager
enclave, which appears to manually handle delegating access to individual enclave seal keys.
- They use
MRENCLAVE
for each indiviual enclave (so the key is bound to the machine) then the enclave can register the actual key with thekeymanager
along with a policy to mediate access more granularly. - Their current policy format allows you to explicitly enumerate which enclaves (
MRENCLAVE
+MRSIGNER
) may query private key material and which enclaves may replicate the "master secret" (something consensus related?).